<?php

/*
 * This file is part of SwiftMailer. (c) 2004-2009 Chris Corbyn For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
 */

/**
 * Allows reading and writing of bytes to and from a file.
 * 
 * @package Swift
 * @subpackage ByteStream
 * @author Chris Corbyn
 */
class Swift_ByteStream_FileByteStream extends Swift_ByteStream_AbstractFilterableInputStream implements Swift_FileStream {
	/**
	 * The internal pointer offset
	 */
	private $_offset = 0;
	
	/**
	 * The path to the file
	 */
	private $_path;
	
	/**
	 * The mode this file is opened in for writing
	 */
	private $_mode;
	
	/**
	 * A lazy-loaded resource handle for reading the file
	 */
	private $_reader;
	
	/**
	 * A lazy-loaded resource handle for writing the file
	 */
	private $_writer;
	
	/**
	 * If magic_quotes_runtime is on, this will be true
	 */
	private $_quotes = false;
	
	/**
	 * If stream is seekable true/false, or null if not known
	 */
	private $_seekable = null;
	
	/**
	 * Create a new FileByteStream for $path.
	 * 
	 * @param string $path        	
	 * @param string $writable
	 *        	if true
	 */
	public function __construct($path, $writable = false) {
		$this->_path = $path;
		$this->_mode = $writable ? 'w+b' : 'rb';
		
		if (function_exists ( 'get_magic_quotes_runtime' ) && @get_magic_quotes_runtime () == 1) {
			$this->_quotes = true;
		}
	}
	
	/**
	 * Get the complete path to the file.
	 * 
	 * @return string
	 */
	public function getPath() {
		return $this->_path;
	}
	
	/**
	 * Reads $length bytes from the stream into a string and moves the pointer
	 * through the stream by $length.
	 * If less bytes exist than are requested the
	 * remaining bytes are given instead. If no bytes are remaining at all, boolean
	 * false is returned.
	 * 
	 * @param int $length        	
	 * @return string
	 * @throws Swift_IoException
	 */
	public function read($length) {
		$fp = $this->_getReadHandle ();
		if (! feof ( $fp )) {
			if ($this->_quotes) {
				ini_set ( 'magic_quotes_runtime', 0 );
			}
			$bytes = fread ( $fp, $length );
			if ($this->_quotes) {
				ini_set ( 'magic_quotes_runtime', 1 );
			}
			$this->_offset = ftell ( $fp );
			
			return $bytes;
		} else {
			$this->_resetReadHandle ();
			
			return false;
		}
	}
	
	/**
	 * Move the internal read pointer to $byteOffset in the stream.
	 * 
	 * @param int $byteOffset        	
	 * @return boolean
	 */
	public function setReadPointer($byteOffset) {
		if (isset ( $this->_reader )) {
			$this->_seekReadStreamToPosition ( $byteOffset );
		}
		$this->_offset = $byteOffset;
	}
	
	// -- Private methods
	
	/**
	 * Just write the bytes to the file
	 */
	protected function _commit($bytes) {
		fwrite ( $this->_getWriteHandle (), $bytes );
		$this->_resetReadHandle ();
	}
	
	/**
	 * Not used
	 */
	protected function _flush() {
	}
	
	/**
	 * Get the resource for reading
	 */
	private function _getReadHandle() {
		if (! isset ( $this->_reader )) {
			if (! $this->_reader = fopen ( $this->_path, 'rb' )) {
				throw new Swift_IoException ( 'Unable to open file for reading [' . $this->_path . ']' );
			}
			if ($this->_offset != 0) {
				$this->_getReadStreamSeekableStatus ();
				$this->_seekReadStreamToPosition ( $this->_offset );
			}
		}
		
		return $this->_reader;
	}
	
	/**
	 * Get the resource for writing
	 */
	private function _getWriteHandle() {
		if (! isset ( $this->_writer )) {
			if (! $this->_writer = fopen ( $this->_path, $this->_mode )) {
				throw new Swift_IoException ( 'Unable to open file for writing [' . $this->_path . ']' );
			}
		}
		
		return $this->_writer;
	}
	
	/**
	 * Force a reload of the resource for reading
	 */
	private function _resetReadHandle() {
		if (isset ( $this->_reader )) {
			fclose ( $this->_reader );
			$this->_reader = null;
		}
	}
	
	/**
	 * Check if ReadOnly Stream is seekable
	 */
	private function _getReadStreamSeekableStatus() {
		$metas = stream_get_meta_data ( $this->_reader );
		$this->_seekable = $metas ['seekable'];
	}
	
	/**
	 * Streams in a readOnly stream ensuring copy if needed
	 */
	private function _seekReadStreamToPosition($offset) {
		if ($this->_seekable === null) {
			$this->_getReadStreamSeekableStatus ();
		}
		if ($this->_seekable === false) {
			$currentPos = ftell ( $this->_reader );
			if ($currentPos < $offset) {
				$toDiscard = $offset - $currentPos;
				fread ( $this->_reader, $toDiscard );
				
				return;
			}
			$this->_copyReadStream ();
		}
		fseek ( $this->_reader, $offset, SEEK_SET );
	}
	
	/**
	 * Copy a readOnly Stream to ensure seekability
	 */
	private function _copyReadStream() {
		if ($tmpFile = fopen ( 'php://temp/maxmemory:4096', 'w+b' )) {
			/* We have opened a php:// Stream Should work without problem */
		} elseif (function_exists ( 'sys_get_temp_dir' ) && is_writable ( sys_get_temp_dir () ) && ($tmpFile = tmpfile ())) {
			/* We have opened a tmpfile */
		} else {
			throw new Swift_IoException ( 'Unable to copy the file to make it seekable, sys_temp_dir is not writable, php://memory not available' );
		}
		$currentPos = ftell ( $this->_reader );
		fclose ( $this->_reader );
		$source = fopen ( $this->_path, 'rb' );
		if (! $source) {
			throw new Swift_IoException ( 'Unable to open file for copying [' . $this->_path . ']' );
		}
		fseek ( $tmpFile, 0, SEEK_SET );
		while ( ! feof ( $source ) ) {
			fwrite ( $tmpFile, fread ( $source, 4096 ) );
		}
		fseek ( $tmpFile, $currentPos, SEEK_SET );
		fclose ( $source );
		$this->_reader = $tmpFile;
	}
}
